package jm.easyconfig;

import java.io.Serializable;
import java.sql.Connection;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import javax.annotation.PostConstruct;
import javax.sql.DataSource;

import org.springframework.context.ApplicationContext;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.util.StringUtils;

public class MySqlConfigSource {
	String CLIENT = "main";
	SqlHelper sqlHelper;
	CfgContextEnvironmentAware contextAware;

	public MySqlConfigSource() {

	}

	public MySqlConfigSource(String client) {
		CLIENT = client;
	}

	@PostConstruct
	public boolean startup() {
		try {
			this.contextAware = CfgContextEnvironmentAware.getSharedInstance();

			DataSource dataSource = ensureSourceAvailable(CfgContextEnvironmentAware.applicationContext);

			if (dataSource == null)
				return false;

			sqlHelper = new SqlHelper(dataSource);

			String database = ensureSourceConnected();

			ensureSourceInited(database);

			return true;

		} catch (Throwable e) {
			CfgHelper.logger.error(
					"Error!!!, failed to initialized data source, dynamic refresh is disabled,message:{} ",
					e.getMessage());
			return false;
		}
	}

	String ensureSourceConnected() {
		try {
			String ret = (String) sqlHelper.getScalar("select database();");
			return ret;

		} catch (Throwable e) {
			CfgHelper.logger.error("Error while to connect the config datasource. message:{}", e.getMessage());
			throw new RuntimeException(e);
		}
	}

	boolean ensureSourceInited(String database) {
		if (!CfgContextEnvironmentAware.environment.getProperty("easyconfig.auto-init-schema", Boolean.class, true))
			return false;

		// try best to check and initialize data source schema safely.
		// no dangerous statement such as DROP TABLE IF EXISTS `config_item_main`; .
		try {
			String SQL = "select count(*) from information_schema.tables where table_name ='config_item_$easy$' and table_schema=?;"
					.replace("$easy$", CLIENT);

			long ret = (long) sqlHelper.getScalar(SQL, database);

			CfgHelper.logger.debug("SQL:{},database:{} return:{},ret == 1:{}", SQL, database, ret, ret == 1);

			if (ret == 1)
				return false;

			String sql = CfgHelper.readTextFile("/META-INF/easyconfig_schema.sql");
			String[] stats = sql.split(";");
			for (String st : stats) {
				st = st.replace("$easy$", CLIENT);
				sqlHelper.execute(st);
			}

			CfgHelper.logger.info("easyconfig datasource schema does not exist, A new one is created!");

		} catch (Throwable e) {
			CfgHelper.logger.error("Error while initializing schema. message:{}", e.getMessage());
			throw new RuntimeException(e);
		}

		return true;
	}

	static DataSource ensureSourceAvailable(ApplicationContext applicationContext) {
		Environment env = applicationContext.getEnvironment();

		DataSource datasource = null;
		String url = env.getProperty("easyconfig.datasource.url");

		if (url != null) {
			try {
				DriverManagerDataSource ds = new DriverManagerDataSource();
				ds.setUrl(url);

				String username = env.getProperty("easyconfig.datasource.username");
				if (username != null) {
					ds.setUsername(username);
				}

				String password = env.getProperty("easyconfig.datasource.password");
				if (password != null) {
					ds.setPassword(password);
				}

				ds.setDriverClassName(
						env.getProperty("easyconfig.datasource.driver-class-name", "com.mysql.cj.jdbc.Driver"));

				CfgHelper.logger.info("Config service connecting to {} .", url);

				datasource = ds;

			} catch (Throwable e) {
				CfgHelper.logger.error("Error when to connect data source url {}.\n message:{}", url, e.getMessage());
			}
		}

		// try data source in application context if data source is not specified or
		// exception thrown
		if (datasource == null) {
			String[] dsNames = applicationContext.getBeanNamesForType(DataSource.class, false, false);

			if (dsNames.length > 0) {
				datasource = (DataSource) applicationContext.getBean(dsNames[0]);
				CfgHelper.logger.info("Config service connecting to system default data source. {}", datasource);
			} else {
				CfgHelper.logger.error("Warning! no datasource exists, config connection cann't been built.");
			}

			if (dsNames.length > 1) {
				CfgHelper.logger.warn(
						"Warning! [{}] datasource exist, configure easyconfig.datasource.* to correct it if target datasource is wrong.",
						dsNames.length);
			}
		}

		if (datasource == null) {
			CfgHelper.logger
					.error("Dynamic config refresh was disabled because data source was found for easy config.");
		}

		return datasource;
	}

	public List<ConfigItem> configList() {
		List<ConfigItem> lst = new LinkedList<>();

		sqlHelper.queryMap(m -> {
			try {
				ActionConfigItem item = new ActionConfigItem().load(m);
				Object $backend = item.item_schema.get("$backend");
				if ($backend == null || !(boolean) $backend) {
					lst.add(item);
				}
			} catch (Throwable ex) {
				CfgHelper.logger.error("Error to load {}, message is:{}.", m, ex.getMessage());
			}
		}, "select item_profile,item_code,item_name,item_value,item_schema,item_status,UNIX_TIMESTAMP(updated_time) * 1000 as updated_time,updated_user from config_item_$easy$ where MOD(item_status,2) = 0 order by item_profile, item_code;"
				.replace("$easy$", CLIENT));

		return lst;
	}

	ConfigItem loadConfigItem(String item_code) {
		return loadConfigItem(item_code, null);
	}
	
	ConfigItem loadConfigItem(String item_code, ConfigItem defVal) {
//		return loadConfigItem("default", item_code, null);

		ConfigItem val = sqlHelper.processInTransaction(conn -> {
			return loadConfigItem(conn, "default", item_code);
		});

		return val == null ? defVal : val;
	}

//	ConfigItem loadConfigItem(String item_profile, String item_code, ConfigItem defVal) {
//		List<Map<String, Object>> lst = sqlHelper.queryMapList(
//				"select item_profile,item_code,item_name,item_value,item_schema,item_status,UNIX_TIMESTAMP(updated_time) * 1000 as updated_time,updated_user,updated_history from config_item_$easy$ where item_profile = ? and item_code = ?;"
//						.replace("$easy$", CLIENT),
//				item_profile, item_code);
//
//		if (lst.size() == 0)
//			return defVal;
//
//		return new ConfigItem().load(lst.get(0));
//	}

//	int updateConfigItem(ConfigItem newItem) {
//		return sqlHelper.execute(
//				"update config_item_$easy$ set item_name = ?,item_value = ?, item_schema = ?, item_status = ?, updated_user = ?, updated_history = ? where item_profile = ? and item_code = ?;"
//						.replace("$easy$", CLIENT),
//				newItem.item_name, newItem.item_value, CfgHelper.jsonWrite(newItem.item_schema), newItem.item_status,
//				newItem.updated_user, newItem.updated_history, newItem.item_profile, newItem.item_code);
//	}

	ConfigItem loadConfigItem(Connection conn, String item_profile, String item_code) {
		Holder<ConfigItem> holder = new Holder<ConfigItem>();

		sqlHelper.queryMap(conn, map -> {
			holder.value = new ConfigItem().load(map);
		}, "select item_profile,item_code,item_name,item_value,item_schema,item_status,UNIX_TIMESTAMP(updated_time) * 1000 as updated_time,updated_user,updated_history from config_item_$easy$ where item_profile = ? and item_code = ?;"
				.replace("$easy$", CLIENT), item_profile, item_code);

		return holder.value;
	}

	int updateConfigItem(ConfigItem newItem) {
		return sqlHelper.processInTransaction(conn -> {
			ConfigItem oldItem = loadConfigItem(conn, newItem.item_profile, newItem.item_code);
			if (oldItem == null) {
				return sqlHelper.execute(conn,
						"insert into config_item_$easy$ (item_profile,item_code,item_name,item_value,item_schema,item_status,updated_user,updated_history) VALUES(?,?,?,?,?,?,?,?);"
								.replace("$easy$", CLIENT),
						newItem.item_profile, newItem.item_code, newItem.item_name, newItem.item_value,
						CfgHelper.jsonWrite(newItem.item_schema), newItem.item_status, newItem.updated_user,
						newItem.updated_history);
			} else {
				return sqlHelper.execute(conn,
						"update config_item_$easy$ set item_name = ?,item_value = ?, item_schema = ?, item_status = ?, updated_user = ?, updated_history = ? where item_profile = ? and item_code = ?;"
								.replace("$easy$", CLIENT),
						newItem.item_name, newItem.item_value, CfgHelper.jsonWrite(newItem.item_schema),
						newItem.item_status, newItem.updated_user, newItem.updated_history, newItem.item_profile,
						newItem.item_code);
			}
		});
	}

	public ItemUpdatedHistory itemUpdatedHistory(String item_code) {
		return itemUpdatedHistory("default", item_code);
	}

	public ItemUpdatedHistory itemUpdatedHistory(String item_profile, String item_code) {

		Object ret = sqlHelper
				.getScalar("select updated_history from config_item_$easy$ where item_profile = ? and item_code = ?;"
						.replace("$easy$", CLIENT), item_profile, item_code);

		return CfgHelper.jsonRead(ret.toString(), ItemUpdatedHistory.class);

	}

	public List<ConfigItemCore> queryAfterTime(Holder<Long> timeHolder) {
		if (this.sqlHelper == null)
			return new LinkedList<ConfigItemCore>();

		List<ConfigItemCore> list = new LinkedList<ConfigItemCore>();

		sqlHelper.queryResultSet(rs -> {
			try {
				ConfigItemCore item = new ConfigItemCore();

				item.withProfile(rs.getString("item_profile")).withCode(rs.getString("item_code"))
						.withValue(rs.getString("item_value")).withStatus(rs.getInt("item_status"));

				list.add(item);

				long ts = rs.getLong("updated_time");
				if (timeHolder.value < ts) {
					timeHolder.value = ts;
				}

			} catch (SQLException e) {
				CfgHelper.logger.error("wrong while querying last updated config.\nmessage:{}", e.getMessage());
			}

		}, "select item_profile, item_code,item_value,item_schema,item_status,UNIX_TIMESTAMP(updated_time)*1000 as updated_time from config_item_$easy$ where updated_time > FROM_UNIXTIME(?);"
				.replace("$easy$", CLIENT), timeHolder.value / 1000);

		return list;
	}

	static class ChangeSubmitArgs {
		public ChangeStatus action;
		public String remark;
		public ActionConfigItem[] array;

		ChangeSubmitArgs withAction(ChangeStatus action) {
			this.action = action;
			return this;
		}

		ChangeSubmitArgs withArray(ActionConfigItem... array) {
			this.array = array;
			return this;
		}

	}

	static class ChangeManageArgs {
		public ChangeStatus action;
		public String remark;
		public int changeId;

		ChangeManageArgs withAction(ChangeStatus action) {
			this.action = action;
			return this;
		}

		ChangeManageArgs withChangeId(int changeId) {
			this.changeId = changeId;
			return this;
		}
	}

	/*
	 * plan | store | publish
	 */
	Object submitChange(ChangeSubmitArgs args) {
		return sqlHelper.processInTransaction(conn -> {
			ConfigChangeItem changeItem = this.logIntoChange(conn, args);

			if (changeItem == null) {
				throw new ApplicationException("no configuration parameter is updated.");
			}

			if (args.action == ChangeStatus.stored || args.action == ChangeStatus.published) {

				this.storeIntoSource(conn, changeItem);

				if (args.action != changeItem.status()) {
					changeItem.withStatus(args.action);
					this.updateChangeItem(conn, changeItem);
				}

				return new UpdateSourceReturn().withChangedItem(changeItem)
						.withUpdatedResult(this.changeResult(changeItem.changed_content));
			}
			return changeItem;
		});
	}

	/*
	 * cancel | store | publish
	 */
	Object manageChange(ChangeManageArgs args) {
		return sqlHelper.processInTransaction(conn -> {

			ConfigChangeItem changeItem = this.loadChangeItem(conn, args.changeId);

			this.assureChangeIsAllowed(Arrays.asList(changeItem.changed_content),
					args.action == ChangeStatus.canceled ? AdminUser.AUTH_CHANGE | AdminUser.AUTH_PUBLISH
							: AdminUser.AUTH_PUBLISH,
					false);

			if (args.action == ChangeStatus.canceled) {

				changeItem.changed_status = ChangeStatus.canceled.ordinal();

				changeItem.changed_summary.addRemark(args.action.name(), args.remark);

				return this.updateChangeItem(conn, changeItem);

			} else if (args.action == ChangeStatus.stored || args.action == ChangeStatus.published) {

				this.storeIntoSource(conn, changeItem);

				changeItem.changed_summary.addRemark(args.action.name(), args.remark);
				changeItem.withStatus(args.action);
				this.updateChangeItem(conn, changeItem);

				return new UpdateSourceReturn().withChangedItem(changeItem)
						.withUpdatedResult(this.changeResult(changeItem.changed_content));
			}

			return changeItem;
		});

	}

	ConfigChangeItem logIntoChange(Connection conn, ChangeSubmitArgs args) {
		int changedId = latestChangeId() + 1;
		String changedUser = CfgAdminServlet.threadContext().username();
		LinkedList<ChangeLogItem> changeLogList = new LinkedList<ChangeLogItem>();

		HashMap<String, ConfigItem> sourceConfig = new HashMap<String, ConfigItem>();

		sqlHelper.queryMap(conn, row -> {
			ConfigItem ci = new ConfigItem().load(row);
			sourceConfig.put(ci.key(), ci);
		}, "select item_profile,item_code,item_name,item_value,item_schema,item_status, UNIX_TIMESTAMP(updated_time)*1000 as updated_time,updated_history from config_item_$easy$;"
				.replace("$easy$", CLIENT));

		for (ActionConfigItem newItem : args.array) {
			ConfigItem sourceItem = sourceConfig.get(newItem.key());

			ChangeLogItem changeLogItem = new ChangeLogItem();
			changeLogItem.withAct(newItem.action.toString()).withProfile(newItem.item_profile)
					.withCode(newItem.item_code).withNname(newItem.item_name).withNval(newItem.item_value)
					.withNstat(newItem.item_status).withNschema(newItem.item_schema);

			if (sourceItem != null)
				changeLogItem.withOname(sourceItem.item_name).withOval(sourceItem.item_value)
						.withOstat(sourceItem.item_status).withOschema(sourceItem.item_schema)
						.withOtms(sourceItem.updated_time);

			switch (newItem.action) {
			case created:
				if (sourceItem == null || ConfigItemCore.isDeleted(sourceItem.item_status)) {
					changeLogList.add(changeLogItem);
				} else {
					CfgHelper.logger.warn("Config item existed in data source when to create {} .", newItem);
				}
				break;
			case updated:
				if (sourceItem == null || ConfigItemCore.isDeleted(sourceItem.item_status)) {
					CfgHelper.logger.warn("Config item don't exist in data source when to {} {} .",
							newItem.action.name(), newItem);
				} else if (Objects.equals(sourceItem.item_value, newItem.item_value)) {
					CfgHelper.logger.info("Config item's new and old value are same when to update {}.", newItem);
				} else {
					changeLogItem.withNname(sourceItem.item_name).withNschema(sourceItem.item_schema);
					changeLogList.add(changeLogItem);
				}
				break;
			case setup:
				if (sourceItem == null || ConfigItemCore.isDeleted(sourceItem.item_status)) {
					CfgHelper.logger.warn("Config item don't exist in data source when to {} {} .",
							newItem.action.name(), newItem);
				} else
					changeLogList.add(changeLogItem);

				break;
			case deleted:
				if (sourceItem == null || ConfigItemCore.isDeleted(sourceItem.item_status)) {
					CfgHelper.logger.warn("Config item don't exist in data source when to {} {} .",
							newItem.action.name(), newItem);
				} else {
					changeLogItem.withNval(sourceItem.item_value).withNname(sourceItem.item_name)
							.withNschema(sourceItem.item_schema);
					changeLogItem.withNstat(ConfigItemCore.markDeleted(changeLogItem.nstat));
					changeLogList.add(changeLogItem);
				}
				break;
			default:// unchanged or wrong
				CfgHelper.logger.debug("No action for config item although it is submited to update. {}", newItem);
			}
		} // end of one of change

		// insert the history
		if (changeLogList.size() > 0) {

			this.assureChangeIsAllowed(changeLogList, args.action == ChangeStatus.planed ? AdminUser.AUTH_CHANGE
					: AdminUser.AUTH_CHANGE | AdminUser.AUTH_PUBLISH, true);

			this.assureChangeNotConflicted(conn, changeLogList);

			sqlHelper.execute(conn,
					"INSERT INTO config_item_$easy$_history(changed_id,changed_summary,changed_content,changed_status,changed_user)VALUES(?,?,?,?,?);"
							.replace("$easy$", CLIENT),
					changedId,
					CfgHelper.jsonWrite(new ChangeSummary().withCount(changeLogList.size()).withProfiles(changeLogList)
							.addRemark(args.action.name(), args.remark)),
					CfgHelper.jsonWrite(changeLogList), args.action.ordinal(), changedUser);// ChangeStatus.planed.ordinal()

			clearOldChangeHistory();

			CfgHelper.logger.info("Config data source is updated. {}", changeLogList);

			return this.loadChangeItem(conn, changedId);

		} else
			CfgHelper.logger.info("Nothing of config data source is updated.");

		return null;
	}

	private void assureChangeIsAllowed(List<ChangeLogItem> changeLogList, int auth, boolean andNotOr) {
		AdminUser user = CfgAdminServlet.threadContext().getUser();
		if (user != null)
			user.changeRequire(changeLogList, auth, andNotOr);
		else
			CfgHelper.logger.error(
					"No logged-in user was found when checking data security. If it is not UnitTest, it is a serious error.");
	}

	void storeIntoSource(Connection conn, ConfigChangeItem changeItem) {
		String changedTime = LocalDateTime.now().withNano(0).toString();
		String changedUser = CfgAdminServlet.threadContext().username();

		HashMap<String, ConfigItem> sourceConfig = new HashMap<String, ConfigItem>();
		sqlHelper.queryMap(conn, row -> {
			ConfigItem ci = new ConfigItem().load(row);
			sourceConfig.put(ci.key(), ci);
		}, "select item_profile,item_code,item_name,item_value,item_schema,item_status,updated_history from config_item_$easy$;"
				.replace("$easy$", CLIENT));

		for (ChangeLogItem changeLog : changeItem.changed_content) {

			ConfigItem sourceItem = sourceConfig.get(changeLog.key());

			String itemLogList = ItemUpdatedHistory.load(sourceItem == null ? null : sourceItem.updated_history)
					.insert(new ItemUpdatedLog().withAct(changeLog.act).withVal(changeLog.oval)
							.withStat(changeLog.ostat).withId(changeItem.changed_id).withTime(changedTime)
							.withUser(changedUser))
					.toString();

			switch (changeLog.act) {
			case "created":
				if (changeLog.otms == 0) {
					sqlHelper.execute(conn,
							"insert into config_item_$easy$ (item_profile,item_code,item_name,item_value,item_schema,item_status,updated_user,updated_history) VALUES(?,?,?,?,?,?,?,?);"
									.replace("$easy$", CLIENT),
							changeLog.prof, changeLog.code, changeLog.nname, changeLog.nval,
							CfgHelper.jsonWrite(changeLog.nschema), changeLog.nstat, changedUser, itemLogList);
				} else {
					sqlHelper.execute(conn,
							"update config_item_$easy$ set item_name = ?,item_value = ?, item_schema = ?, item_status = ?, updated_user = ?, updated_history = ? where item_profile = ? and item_code = ?;"
									.replace("$easy$", CLIENT),
							changeLog.nname, changeLog.nval, CfgHelper.jsonWrite(changeLog.nschema), changeLog.nstat,
							changedUser, itemLogList, changeLog.prof, changeLog.code);
				}
				break;
			case "updated":
			case "setup":
			case "deleted":
				sqlHelper.execute(conn,
						"update config_item_$easy$ set item_name = ?,item_value = ?, item_schema = ?, item_status = ?, updated_user = ?, updated_history = ? where item_profile = ? and  item_code = ?;"
								.replace("$easy$", CLIENT),
						changeLog.nname, changeLog.nval, CfgHelper.jsonWrite(changeLog.nschema), changeLog.nstat,
						changedUser, itemLogList, changeLog.prof, changeLog.code);
				break;
			}
		}

	}

	int clearOldChangeHistory() {
		Integer last = (Integer) sqlHelper.getScalar(
				"select changed_id from config_item_$easy$_history  order by changed_id desc,changed_time desc limit ?,1;"
						.replace("$easy$", CLIENT),
				SysProps.inst.changeHistoryMax);
		if (last == null)
			return 0;

		return sqlHelper.execute(
				"delete from config_item_$easy$_history where changed_id <= ?;".replace("$easy$", CLIENT), last);

	}

	int latestChangeId() {
		Object ret = sqlHelper
				.getScalar("select max(changed_id) from config_item_$easy$_history;".replace("$easy$", CLIENT));
		return ret == null ? 0 : (int) ret;
	}

	ConfigChangeItem loadChangeItem(int id) {
		return sqlHelper.processInTransaction((conn) -> {
			return this.loadChangeItem(conn, id);
		});
	}

	ConfigChangeItem loadChangeItem(Connection conn, int id) {
		Holder<ConfigChangeItem> holder = new Holder<>();
		sqlHelper.queryMap(conn, (map) -> {
			holder.value = new ConfigChangeItem().load(map);
		}, "select changed_id,changed_summary,changed_content,UNIX_TIMESTAMP(changed_time)*1000 as changed_time, changed_user, changed_status from config_item_$easy$_history where changed_id = ?;"
				.replace("$easy$", CLIENT), id);

		return holder.value;
	}

	int updateChangeItem(ConfigChangeItem newItem) {
		return sqlHelper.processInTransaction((conn) -> {
			return this.updateChangeItem(conn, newItem);
		});
	}

	int updateChangeItem(Connection conn, ConfigChangeItem newItem) {
		return sqlHelper.execute(conn,
				"update config_item_$easy$_history set changed_summary = ?,changed_content = ?, changed_user = ?, changed_status = ? where changed_id = ?;"
						.replace("$easy$", CLIENT),
				newItem.changed_summary.toString(), newItem.contentToString(), newItem.changed_user,
				newItem.changed_status, newItem.changed_id);
	}

	public PagerResult<List<ConfigChangeItem>> changeHistory(PageArgs args) {
		List<ConfigChangeItem> lst = new LinkedList<ConfigChangeItem>();

		sqlHelper.queryMap(map -> {
			try {
				lst.add(new ConfigChangeItem().load(map));
			} catch (Throwable ex) {
				CfgHelper.logger.error("Error to load {}, message is:{}.", map, ex.getMessage());
			}
		}, "select changed_id,changed_summary,changed_content,UNIX_TIMESTAMP(changed_time)*1000 as changed_time, changed_user, changed_status from config_item_$easy$_history order by changed_id desc,changed_time desc limit ? , ?;"
				.replace("$easy$", CLIENT), (args.pageNo - 1) * args.pageSize, args.pageSize);

		long totalCount = (Long) sqlHelper
				.getScalar("select count(*) from config_item_$easy$_history;".replace("$easy$", CLIENT));

		PagerResult<List<ConfigChangeItem>> ret = new PagerResult<List<ConfigChangeItem>>();
		ret.list = lst;
		ret.totalCount = totalCount;

		return ret;
	}

	// just for test
	void restoreVersion(int changedId) {
		restoreChange(changedId, true, true, null);
	}

	// just for test
	void restoreChangeAfter(int changedId) {
		restoreChange(changedId, false, false, null);
	}

	// just for test
	void restoreChangeBefore(int changedId) {
		restoreChange(changedId, false, true, null);
	}

	public UpdateSourceReturn restoreChange(int changedId, boolean allProp, boolean rollbackThis, String remark) {
		return sqlHelper.processInTransaction(conn -> {

			LinkedList<ConfigChangeItem> changeHistory = new LinkedList<ConfigChangeItem>();
			sqlHelper.queryMap(conn, row -> {
				changeHistory.addLast(new ConfigChangeItem().load(row));
			}, "select changed_id, changed_content,changed_status from config_item_$easy$_history where changed_id >= ? order by changed_id desc,changed_time desc;"
					.replace("$easy$", CLIENT), changedId);

			ChangeLogItem[] lastChangeLogSet = changeHistory.removeLast().changed_content;

			Map<String, ChangeLogItem> lastChangeLogMap = null;
			if (!allProp) {
				lastChangeLogMap = new HashMap<String, ChangeLogItem>();
				for (ChangeLogItem item : lastChangeLogSet) {
					lastChangeLogMap.put(item.code, item);
				}
			}

			LinkedList<ChangeLogItem> actionList = new LinkedList<ChangeLogItem>();
			for (ConfigChangeItem changeItem : changeHistory) {
				if (changeItem.status() == ChangeStatus.canceled)
					continue;

				ChangeLogItem[] set = changeItem.changed_content;

				for (ChangeLogItem item : set) {
					if (allProp || lastChangeLogMap.containsKey(item.code)) {
						actionList.addLast(reverseChangeLog(item));
					}
				}
			}

			if (rollbackThis)
				for (ChangeLogItem item : lastChangeLogSet) {
					actionList.addLast(reverseChangeLog(item));
				}

			if (actionList.size() == 0)
				return null;

			this.assureChangeIsAllowed(actionList, AdminUser.AUTH_CHANGE | AdminUser.AUTH_PUBLISH, true);

			this.assureChangeNotConflicted(conn, actionList);

			int changeId = this.latestChangeId() + 1;
			sqlHelper.execute(conn,
					"INSERT INTO config_item_$easy$_history(changed_id,changed_summary,changed_content,changed_status,changed_user)VALUES(?,?,?,?,?);"
							.replace("$easy$", CLIENT),
					changeId,
					CfgHelper.jsonWrite(new ChangeSummary().withCount(actionList.size()).withProfiles(actionList)
							.addRemark(ChangeStatus.published.name(), remark)),
					CfgHelper.jsonWrite(actionList), ChangeStatus.published.ordinal(),
					CfgAdminServlet.threadContext().username());

			clearOldChangeHistory();

			ConfigChangeItem changeItem = this.loadChangeItem(conn, changeId);

			CfgHelper.logger.info(
					"Config is restored to change id {}, include all properties:{},include this change:{}. ", changedId,
					allProp, rollbackThis);

			this.storeIntoSource(conn, changeItem);

			return new UpdateSourceReturn().withChangedItem(changeItem)
					.withUpdatedResult(this.changeResult(changeItem.changed_content));
		});
	}

	ChangeLogItem reverseChangeLog(ChangeLogItem record) {
		ChangeLogItem action = new ChangeLogItem();
		action.prof = record.prof;
		action.code = record.code;
		action.nval = record.oval;
		action.oval = record.nval;
		action.nstat = record.ostat;
		action.ostat = record.nstat;
		action.nname = record.oname;
		action.oname = record.nname;
		action.nschema = record.oschema;
		action.oschema = record.nschema;

		action.otms = -1;

		switch (record.act) {
		case "created":
			action.act = ActionType.deleted.name();
			break;
		case "updated":
			action.act = ActionType.updated.name();
			break;
		case "setup":
			action.act = ActionType.setup.name();
			break;
		case "deleted":
			action.act = ActionType.created.name();
			break;
		default:
			CfgHelper.logger.warn("Wrong action type while restoring history change. {}", record);
		}

		return action;
	}

	List<ConfigItemCore> changeResult(ChangeLogItem[] array) {
		HashMap<String, ConfigItemCore> updatedResult = new HashMap<String, ConfigItemCore>();

		for (ChangeLogItem changeLog : array) {
			updatedResult.put(changeLog.key(), new ConfigItemCore().withProfile(changeLog.prof).withCode(changeLog.code)
					.withValue(changeLog.nval).withStatus(changeLog.nstat));
		}

		LinkedList<ConfigItemCore> lst = new LinkedList<ConfigItemCore>();
		lst.addAll(updatedResult.values());

		return lst;
	}

	/*
	 * item_code,changeId,changeStatus
	 */
	static class CheckResultRecord {
		public String itemProfile;
		public String itemCode;
		public int changeId;
		public ChangeStatus status;

		CheckResultRecord withItemProfile(String itemProfile) {
			this.itemProfile = itemProfile;
			return this;
		}

		CheckResultRecord withItemCode(String itemCode) {
			this.itemCode = itemCode;
			return this;
		}

		CheckResultRecord withChangeId(int changeId) {
			this.changeId = changeId;
			return this;
		}

		CheckResultRecord withChangeStatus(ChangeStatus changeStatus) {
			this.status = changeStatus;
			return this;
		}

		String key() {
			return itemProfile + "+" + itemCode + "@" + changeId;
		}

	}

	void assureChangeNotConflicted(Connection conn, List<ChangeLogItem> list) {

		LinkedList<ConfigChangeItem> changeHistory = new LinkedList<ConfigChangeItem>();

		sqlHelper.queryMap(conn, row -> {
			changeHistory.addLast(new ConfigChangeItem().load(row));
		}, "select changed_id, changed_content,changed_status from config_item_$easy$_history where changed_status in (?,?,?) order by changed_id desc,changed_time desc;"
				.replace("$easy$", CLIENT), ChangeStatus.planed.ordinal(), ChangeStatus.stored.ordinal(),
				ChangeStatus.graying.ordinal());

		HashMap<String, CheckResultRecord> result = new HashMap<String, CheckResultRecord>();

		for (ConfigChangeItem change : changeHistory) {
			for (ChangeLogItem log : change.changed_content) {
				for (ChangeLogItem now : list) {
					if (log.code.equals(now.code)) {

						CheckResultRecord rcd = new CheckResultRecord().withItemCode(now.code)
								.withChangeId(change.changed_id).withChangeStatus(change.status());

						result.put(rcd.key(), rcd);
					}
				}
			}
		}

		if (result.size() > 0)
			throw new OngoingConflictException(result.values());

	}
}

/**
 * this class definition is copied from javax.xml.ws.Holder, just for more
 * compatible in module system of java 9 above.
 * 
 * @author wangjiamin
 *
 * @param <T>
 */
final class Holder<T> implements Serializable {

	private static final long serialVersionUID = 2623699057546497185L;

	/**
	 * The value contained in the holder.
	 */
	public T value;

	/**
	 * Creates a new holder with a <code>null</code> value.
	 */
	public Holder() {
	}

	/**
	 * Create a new holder with the specified value.
	 *
	 * @param value The value to be stored in the holder.
	 */
	public Holder(T value) {
		this.value = value;
	}
}
